<!DOCTYPE html>
<!--
Copyright 2020 The Periph Authors. All rights reserved.
Use of this source code is governed under the Apache License, Version 2.0
that can be found in the LICENSE file.
-->
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="blue" />
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<meta name="description" content="Periph web UI">
<meta name="author" content="Periph Authors">
<title>periph-web</title>
<style>
* {
  font-family: sans-serif;
  font-size: 14px;
}
h1 {
  font-size: 24px;
}
h2 {
  font-size: 20px;
}
h3 {
  font-size: 16px;
}
h1, h2, h3 {
  margin-bottom: 0.2em;
  margin-top: 0.2em;
}
.err {
  background: #F44;
  border: 1px solid #888;
  border-radius: 10px;
  padding: 10px;
  display: none;
}

@media only screen and (max-width: 500px) {
  * {
    font-size: 12px;
  }
}
</style>

<!-- *** Javascript logic *** -->

<script>
"use strict";
/* *** Generic Javascript tools *** */

// log logs to the console, only if enabled.
function log(v) {
  //console.log(v);
}

// Pure javascript event system that implements EventTarget.
// https://devdocs.io/dom/eventtarget
class EventSource {
  constructor() {
    this._triggers = {};
  }
  // https://devdocs.io/dom/eventtarget/addeventlistener
  addEventListener(type, listener, options) {
    if (!this._triggers[type]) {
      this._triggers[type] = [];
    }
    let opt = options || {};
    let v = {
      capture: opt.capture,
      listener: listener,
      once: opt.once,
      passive: opt.passive,
    };
    this._triggers[type].push(v);
  }
  // https://devdocs.io/dom/eventtarget/removeeventlistener
  removeEventListener(type, listener, options) {
    if (!this._triggers[type]) {
      return;
    }
    let l = this._triggers[type].slice();
    let opt = options || {};
    for (let i = l.length; i > 0; i--) {
      let v = l[i-1];
      if (v.callback === callback &&
          v.capture === opt.capture &&
          v.passive === opt.passive) {
        this._triggers[type].pop(i);
      }
    }
  }
  // https://devdocs.io/dom/eventtarget/dispatchevent
  dispatchEvent(type, params) {
    log("dispatchEvent("+type+", "+params+")");
    let l = this._triggers[type];
    if (!l) {
      return;
    }
    // The challenge here is that during the dispatch, an addEventListener()
    // may have been called. This needs to be handled explicitly.
    let rm = [];
    for (let i = 0; i < l.length; i++) {
      let opt = l[i];
      opt.listener.call(params);
      if (opt.once) {
        rm.push(opt);
      }
    }
    for (let i = 0; i < rm.length; i++) {
      // This is inefficient but safe.
      for (let j = 0; j < l.length; l++) {
        if (l[j] === rm[i]) {
         l.pop(j);
         break;
       }
      }
    }
  }
}

// postJSON sends a HTTPS POST to a JSON API and calls the callback with the
// decoded JSON reply.
function postJSON(url, data, callback) {
  function checkStatus(res) {
    if (res.status == 401) {
      throw new Error("Please refresh the page");
    }
    if (res.status >= 200 && res.status < 300) {
      return res.json();
    }
    throw new Error(res.statusText);
  }
  function onError(url, err) {
    console.log(err);
    alertError(url + ": " + err.toString());
  }
  let hdr = {
    body: JSON.stringify(data),
    credentials: "same-origin",
    headers: {"Content-Type": "application/json; charset=utf-8"},
    method: "POST",
  };
  fetch(url, hdr).then(checkStatus).then(callback).catch(err => onError(url, err));
}

// alertError shows or appends the error message in a top red bubble.
function alertError(errText) {
  let e = document.getElementById("err");
  if (e.innerText) {
    e.innerText = e.innerText + "\n";
  }
  e.innerText = e.innerText + errText + "\n";
  e.style.display = "block";
}

/* *** periph specific Javascript logic *** */

// Pin is a pin on an header. It could be a GPIO, but it can be a dead pin too.
// Controller eventually resolves these.
class Pin {
  constructor(name, number, func, gpio) {
    this.name = name;
    this.number = number;
    this._func = func;
    this.gpio = gpio;
  }
  get func() {
    if (this.gpio) {
      return this.gpio.func;
    }
    return this._func;
  }
}

// GPIO is a pin that supports digital I/O. A Pin can point to a GPIO.
class GPIO {
  constructor(name, number, func) {
    // Immutable.
    this.name = name;
    this.number = number;
    // Mutable.
    this.func = func;
    this._value = null;
  }
  get value() {
    return this._value;
  }
  onFuncUpdate(f, v) {
    if (this.func === this._makeFunc(f, v)) {
      return false;
    }
    this._value = v;
    this.func = this._makeFunc(f, v);
    return true;
  }
  onValueRead(v) {
    if (this._value === v) {
      return false;
    }
    if (this.type === "out") {
      // This happens when toggling an output GPIO level and a concurrent
      // returns the previous value. Skip it.
      return false;
    }
    this._value = v;
    this.func = this._makeFunc(this.type, v);
    return true;
  }
  get type() {
    if (this.func.startsWith("Out/")) {
      return "out";
    }
    if (this.func.startsWith("In/")) {
      return "in";
    }
    return this.func;
  }
  _makeFunc(t, v) {
    if (t == "in") {
      if (v === true) {
        return "In/High";
      } else if (v === false) {
        return "In/Low";
      }
      return "In/Ind";
    } else if (t == "out") {
      if (v === true) {
        return "Out/High";
      } else if (v === false) {
        return "Out/Low";
      }
      return "Out/Ind";
    }
    return t;
  }
}

// Header is a collection of Pin on a board.
class Header {
  constructor(name, pins) {
    this.name = name;
    // [[Pin]]
    this.pins = pins;
  }
}

// Controller is the app controller. It is a singleton.
var Controller = new class {
  constructor() {
    // Global events.
    // A GPIO is found or updated. The event name is the name of the GPIO.
    this.eventGPIO = new EventSource();
    // A form of object is done loading.
    this.eventDone = new EventSource();

    // Global data.
    // {name: GPIO}
    this.gpios = {};
    // {name: Header}
    this.headers = {};

    // State transitions.
    // Pins that are being polled. Only pins set as In are polled.
    // {name: true}
    this._polling = {};
    // timer ID to be able to cancel it.
    this._pollingID = null;
    // Rate in ms. 10Hz is a bit intense but until we have an API that is purely
    // event based via gpio.PinIn.WaitForEdge(), that's the best we can do.
    this._pollingRate = 100;

    // Initialization.
    document.addEventListener("DOMContentLoaded", () => {
      // Both are asynchronous.
      this._fetchGPIO();
      this._fetchHeader();
    }, {once: true});
  }
  setGPIOIn(gpio) {
    log("setGPIOIn("+gpio.name+")");
    let params = {
      Name: gpio.name,
      Pull: "",
      Edge: "",
    };
    gpio.onFuncUpdate("in", null);
    // Trigger right away as if it had succeeded to remove latency in the UI.
    this.eventGPIO.dispatchEvent(gpio.name, "in");
    postJSON("/api/periph/v1/gpio/in", [params], res => {
      if (res[0]) {
        alertError(res[0]);
        return;
      }
    });
  }
  setGPIOOut(gpio, l) {
    log("setGPIOOut("+gpio.name+", "+l+")");
    let params = {};
    params[gpio.name] = l;
    gpio.onFuncUpdate("out", l);
    // Trigger right away as if it had succeeded to remove latency in the UI.
    this.eventGPIO.dispatchEvent(gpio.name, "out");
    postJSON("/api/periph/v1/gpio/out", params, res => {
      if (res[0]) {
        alertError(res[0]);
        return;
      }
    });
  }

  // Private code.
  _fetchGPIO() {
    postJSON("/api/periph/v1/gpio/list", {}, res => {
      log("/api/periph/v1/gpio/list");
      for (let i = 0; i < res.length; i++) {
        let name = res[i].Name;
        let gpio = new GPIO(name, res[i].Number, res[i].Func);
        this.gpios[name] = gpio;
        this.eventGPIO.dispatchEvent(name);
      }
      this.eventDone.dispatchEvent("gpio");
    });
  }
  _fetchHeader() {
    postJSON("/api/periph/v1/header/list", {}, res => {
      log("/api/periph/v1/header/list");
      for (let key in res) {
        let pins = [];
        for (let y = 0; y < res[key].Pins.length; y++) {
          let row = res[key].Pins[y];
          let items = [];
          for (let x = 0; x < row.length; x++) {
            let src = row[x];
            // As the Pin instances are connected, look up the corresponding
            // GPIO instance. If it is not present, hook up an event to catch
            // it if one ever show up.
            let p = new Pin(src.Name, src.Number, src.Func, this.gpios[src.Name]);
            // Only poll the GPIOs in an header.
            if (!p.gpio) {
              this.eventGPIO.addEventListener(p.name, () => {
                p.gpio = this.gpios[p.name];
                this._autoPoll(p.gpio);
                this.eventGPIO.addEventListener(p.gpio.name, () => {
                  this._autoPoll(p.gpio);
                });
              }, {once: true});
            } else {
              this._autoPoll(p.gpio);
              this.eventGPIO.addEventListener(p.gpio.name, () => {
                this._autoPoll(p.gpio);
              });
            }
            items[x] = p;
          }
          pins[y] = items;
        }
        this.headers[key] = new Header(key, pins);
      }
      this.eventDone.dispatchEvent("header");
    });
  }
  _autoPoll(gpio) {
    if (gpio.type == "in") {
      log("Now polling " + gpio.name);
      this._polling[gpio.name] = true;
      if (this._pollingID === null) {
        this._pollingID = window.setTimeout(this._refreshGPIO.bind(this), this._pollingRate);
      }
    } else {
      log("Stop polling " + gpio.name);
      delete this._polling[gpio.name];
      if (this._pollingID && !this._polling.length) {
        window.clearTimeout(this._pollingID);
        this._pollingID = null;
      }
    }
  }
  _refreshGPIO() {
    // Keep a copy of the pins that were fetched.
    let pins = Object.keys(this._polling).sort();
    postJSON("/api/periph/v1/gpio/read", pins, res => {
      for (let i = 0; i < pins.length; i++) {
        let p = this.gpios[pins[i]];
        switch (res[i]) {
        case 0:
          if (p.onValueRead(false)) {
            this.eventGPIO.dispatchEvent(p.name, false);
          }
          break;
        case 1:
          if (p.onValueRead(true)) {
            this.eventGPIO.dispatchEvent(p.name, true);
          }
          break;
        default:
          if (p.onValueRead(null)) {
            this.eventGPIO.dispatchEvent(p.name, null);
          }
          break;
        }
      }
      this._pollingID = setTimeout(this._refreshGPIO.bind(this), this._pollingRate);
    });
  }
};

function fetchI2C() {
  postJSON("/api/periph/v1/i2c/list", {}, res => {
    let root = document.getElementById("section-i2c");
    for (let i = 0; i < res.length; i++) {
      let e = root.appendChild(document.createElement("i2c-elem"));
      e.setupI2C(res[i].Name, res[i].Number, res[i].Err, res[i].SCL, res[i].SDA);
    }
  });
}

function fetchSPI() {
  postJSON("/api/periph/v1/spi/list", {}, res => {
    let root = document.getElementById("section-spi");
    for (let i = 0; i < res.length; i++) {
      let e = root.appendChild(document.createElement("spi-elem"));
      e.setupSPI(res[i].Name, res[i].Number, res[i].Err, res[i].CLK, res[i].MOSI, res[i].MISO, res[i].CS);
    }
  });
}

function fetchState() {
  postJSON("/api/periph/v1/server/state", {}, res => {
    document.title = "periph-web - " + res.Hostname;
    let root = document.getElementById("section-drivers-loaded");
    if (!res.State.Loaded.length) {
      root.display = "hidden";
    } else {
      root.setupDrivers(["Drivers loaded"]);
      for (let i = 0; i < res.State.Loaded.length; i++) {
        root.appendRow([res.State.Loaded[i]]);
      }
    }
    root = document.getElementById("section-drivers-skipped");
    if (!res.State.Skipped.length) {
      root.display = "hidden";
    } else {
      root.setupDrivers(["Drivers skipped", "Reason"]);
      for (let i = 0; i < res.State.Skipped.length; i++) {
        root.appendRow([res.State.Skipped[i].D, res.State.Skipped[i].Err]);
      }
    }
    root = document.getElementById("section-drivers-failed");
    if (!res.State.Failed.length) {
      root.display = "hidden";
    } else {
      root.setupDrivers(["Drivers failed", "Error"]);
      for (let i = 0; i < res.State.Failed.length; i++) {
        root.appendRow([res.State.Failed[i].D, res.State.Failed[i].Err]);
      }
    }
  });
}

Controller.eventDone.addEventListener("header", () => {
  // Fill the headers.
  let root = document.getElementById("section-gpio");
  Object.keys(Controller.headers).sort().forEach(key => {
    root.appendChild(document.createElement("header-view")).setupHeader(key);
  });
}, {once: true});

document.addEventListener("DOMContentLoaded", () => {
  fetchI2C();
  fetchSPI();
  fetchState();
}, {once: true});

// HTMLElementTemplate is a base class for a custom element that uses a template
// element and stores it in its shadowDOM.
class HTMLElementTemplate extends HTMLElement {
  constructor(template_name) {
    super();
    let tmpl = document.querySelector("template#" + template_name);
    this.attachShadow({mode: "open"}).appendChild(tmpl.content.cloneNode(true));
  }
  static get observedAttributes() {return [];}
  emitEvent(name, detail) {
    this.dispatchEvent(new CustomEvent(name, {detail, bubbles: true}));
  }
}
</script>

<!-- *** Custom elements *** -->

<!-- Generic custom elements -->

<!-- A generic table -->
<template id="template-data-table-elem">
  <style>
    th {
      background-color: #4CAF50;
      color: white;
    }
    th, td {
      padding: 0.5rem;
      border-bottom: 1px solid #ddd;
    }
    tr:hover {
      background-color: #CCC;
    }
    tr:nth-child(even):not(:hover) {
      background: #f5f5f5;
    }
    .inline {
      display: inline-block;
      margin-bottom: 1rem;
      margin-right: 2rem;
      vertical-align: top;
    }
  </style>
  <div class="inline">
    <table>
      <thead></thead>
      <tbody></tbody>
    </table>
  </div>
</template>
<script>
"use strict";
window.customElements.define("data-table-elem", class extends HTMLElementTemplate {
  constructor() {super("template-data-table-elem");}
  setupTable(hdr) {
    let root = this.shadowRoot.querySelector("thead");
    for (let i = 0; i < hdr.length; i++) {
      root.appendChild(document.createElement("th")).innerText = hdr[i];
    }
  }
  appendRow(line) {
    let tr = this.shadowRoot.querySelector("tbody").appendChild(document.createElement("tr"));
    let items = [];
    for (let i = 0; i < line.length; i++) {
      let cell = tr.appendChild(document.createElement("td"));
      if (line[i] instanceof Element) {
        cell.appendChild(line[i]);
        items[i] = line[i];
      } else {
        cell.innerText = line[i];
        items[i] = cell;
      }
    }
    return items;
  }
});
</script>

<!-- A generic checkbox -->
<template id="template-checkout-elem">
  <style>
    @keyframes popIn {
      0% { transform: scale(1, 1); }
      25% { transform: scale(1.2, 1); }
      50% { transform: scale(1.4, 1); }
      100% { transform: scale(1, 1); }
    }
    @keyframes popOut {
      0% { transform: scale(1, 1); }
      25% { transform: scale(1.2, 1); }
      50% { transform: scale(1.4, 1); }
      100% { transform: scale(1, 1); }
    }
    div {
      display: inline-block;
      height: 20px;
      position: relative;
      vertical-align: bottom;
    }
    input {
      bottom: 0;
      cursor: pointer;
      display: block;
      height: 0%;
      left: 0;
      margin: 0 0;
      opacity: 0;
      position: absolute;
      right: 0;
      top: 0;
      width: 0%;
    }
    span {
      cursor: pointer;
      margin-left: 0.25em;
      padding-left: 40px;
      user-select: none;
    }
    span:before {
      background: rgba(100, 100, 100, .2);
      border-radius: 20px;
      box-shadow: inset 0 0 5px rgba(0, 0, 0, .8);
      content: "";
      display: inline-block;
      height: 20px;
      left: 0px;
      position: absolute;
      transition: background .2s ease-out;
      width: 40px;
    }
    span:after {
      background-clip: padding-box;
      background: #fff;
      border-radius: 20px;
      border: solid green 2px;
      content: "";
      display: block;
      font-weight: bold;
      height: 20px;
      left: -2px;
      position: absolute;
      text-align: center;
      top: -2px;
      transition: margin-left 0.1s ease-in-out;
      width: 20px;
    }
    input:checked + span:after {
      margin-left: 20px;
    }
    input:checked + span:before {
      transition: background .2s ease-in;
    }
    input:not(:checked) + span:after {
      animation: popOut ease-in .3s normal;
    }
    input:checked + span:after {
      animation: popIn ease-in .3s normal;
      background-clip: padding-box;
      margin-left: 20px;
    }
    input:checked + span:before {
      background: #20c997;
    }
    input:disabled + span:before {
      box-shadow: 0 0 0 black;
    }
    input:disabled + span {
      color: #adb5bd;
    }
    input:disabled:checked + span:before {
      background: #adb5bd;
    }
    input:indeterminate + span:after {
      margin-left: 10px;
    }
    input:focus + span:before {
      outline: solid #cce5ff 2px;
    }
  </style>
  <div>
    <label>
      <input type="checkbox"><span><slot></slot></span>
    </label>
  </div>
</template>
<script>
"use strict";
window.customElements.define("checkout-elem", class extends HTMLElementTemplate {
  constructor() {super("template-checkout-elem");}
  connectedCallback() {
    // Note: I tried to have this custom element sync the embedded checkbox
    // attributes "checked", "disabled" and "indeterminate" through
    // observedAttributes() and attributeChangedCallback() but I failed. This
    // would be useful to style <checkout-elem> based on its state.
    // A contribution for this would be appreciated!
    this.contentElem = this.shadowRoot.querySelector("span");
    this.checkboxElem = this.shadowRoot.querySelector("input");
    this.checkboxElem.addEventListener("click", e => {
      // Trigger "change" instead of "click" because click mistriggers.
      this.emitEvent("change", {});
    }, {passive: true});
  }
  get checked() {
    return this.checkboxElem.checked;
  }
  set checked(v) {
    this.checkboxElem.checked = v;
  }
  get disabled() {
    return this.checkboxElem.disabled;
  }
  set disabled(v) {
    this.checkboxElem.disabled = v;
  }
  get indeterminate() {
    return this.checkboxElem.indeterminate;
  }
  set indeterminate(v) {
    this.checkboxElem.indeterminate = v;
  }
  get text() {
    return this.contentElem.innerText;
  }
  set text(v) {
    this.contentElem.innerText = v;
  }
});
</script>

<!-- periph specific custom elements -->

<!-- List of drivers -->
<template id="template-drivers-elem">
  <style>
    .inline {
      display: inline-block;
    }
  </style>
  <div class="inline">
    <data-table-elem></data-table-elem>
  </div>
</template>
<script>
"use strict";
window.customElements.define("drivers-elem", class extends HTMLElementTemplate {
  constructor() {super("template-drivers-elem");}
  setupDrivers(hdr) {
    this.shadowRoot.querySelector("data-table-elem").setupTable(hdr);
  }
  appendRow(row) {
    this.shadowRoot.querySelector("data-table-elem").appendRow(row);
  }
});
</script>

<!-- A single Pin or GPIO -->
<template id="template-gpio-view">
  <style>
  div {
    background: #CCC;
    border: 1px solid #888;
    border-radius: 10px;
    padding: 10px;
  }
  .gpio {
    background: #FCF;
  }
  .controls {
    display: none;
  }
  .box {
    border: 1px solid #888;
    border-radius: 6px;
    padding: 3px;
  }
  #func {
    display: none;
    background-color: #ccc;
    padding-bottom: 3px;
    padding-right: 3px;
    border-radius: 3px;
  }
  </style>
  <div>
    <span id="name">L</span>
    <span>
      <span class="controls">
        <span>
          <checkout-elem id="io">I/O</checkout-elem>
          <checkout-elem id="level">Level</checkout-elem>
        </span>
      </span>
      <span id="func"></span>
    </span>
  </div>
</template>
<script>
"use strict";
window.customElements.define("gpio-view", class extends HTMLElementTemplate {
  constructor() {super("template-gpio-view");}
  connectedCallback() {
    this.funcElem = this.shadowRoot.getElementById("func");
    this.ioElem = this.shadowRoot.getElementById("io");
    this.levelElem = this.shadowRoot.getElementById("level");
    // This uses "change" instead of "click" because click mistriggers.
    this.ioElem.addEventListener("change", e => {
      log(this.id + ".io.change("+this.ioElem.checked+")");
      if (this.ioElem.checked) {
        Controller.setGPIOOut(this.pin.gpio, this.levelElem.checked);
      } else {
        Controller.setGPIOIn(this.pin.gpio);
      }
    }, {passive: true});
    this.levelElem.addEventListener("change", e => {
      log(this.id + ".level.change("+this.levelElem.checked+")");
      if (this.ioElem.checked) {
        Controller.setGPIOOut(this.pin.gpio, this.levelElem.checked);
      }
    }, {passive: true});
  }
  setupPin(pin) {
    this.pin = pin;
    this.id = this.pin.name;
    this.shadowRoot.getElementById("name").textContent = this.pin.name;
    if (this.pin.gpio) {
      log(this.id + " is GPIO");
      this._isGPIO();
      return;
    }
    log(this.id + " is indeterminate");
    if (this.pin.func) {
      this.funcElem.textContent = this.pin.func;
      this.funcElem.style.display = "inline-block";
    }
    // It could become a GPIO later when the /gpio/list RPC comes back.
    Controller.eventGPIO.addEventListener(this.pin.name, () => {
      log(this.id + " is GPIO (late)");
      this._isGPIO();
    }, {once: true});
  }
  _isGPIO() {
    Controller.eventGPIO.addEventListener(this.pin.name, () => this._gpioUpdate());
    this.shadowRoot.querySelector("div").classList.add("gpio");
    this._gpioUpdate();
  }
  _gpioUpdate() {
    log(this.id+"._gpioUpdate("+this.pin.func+")")
    // Assumes a GPIO has a function.
    if (this.pin.func.startsWith("In/") || this.pin.func.startsWith("Out/")) {
      this.funcElem.textContent = "";
      this.funcElem.style.display = "none";
      this.shadowRoot.querySelector(".controls").style.display = "inline-block";

      this.ioElem.checked = this.pin.func.startsWith("Out/");
      this.ioElem.disabled = false;
      this.ioElem.indeterminate = false;
      this.levelElem.checked = this.pin.func.endsWith("/High");
      this.levelElem.disabled = !this.ioElem.checked;

      if (this.ioElem.checked) {
        this.ioElem.text = "Out";
      } else {
        this.ioElem.text = "In";
      }
      if (this.levelElem.checked) {
        this.levelElem.text = "High";
        this.levelElem.indeterminate = false;
      } else if (this.pin.func.endsWith("/Low")) {
        this.levelElem.text = "Low";
        this.levelElem.indeterminate = false;
      } else {
        this.levelElem.indeterminate = true;
        this.levelElem.text = "Ind";
      }
    } else {
      if (this.pin.func) {
        this.funcElem.textContent = this.pin.func;
        this.funcElem.style.display = "inline-block";
      } else {
        this.funcElem.textContent = "";
        this.funcElem.style.display = "none";
      }
      this.ioElem.checked = false;
      this.ioElem.disabled = true;
      this.ioElem.indeterminate = true;
      this.ioElem.text = "I/O";
      this.levelElem.checked = false;
      this.levelElem.disabled = true;
      this.levelElem.indeterminate = true;
      this.levelElem.text = "Level";
    }
  }
});
</script>

<!-- A single Header -->
<template id="template-header-view">
  <data-table-elem></data-table-elem>
</template>
<script>
"use strict";
window.customElements.define("header-view", class extends HTMLElementTemplate {
  constructor() {super("template-header-view");}
  setupHeader(name) {
    this.header = Controller.headers[name];
    let data = this.shadowRoot.querySelector("data-table-elem");
    let cols = 1;
    if (this.header.pins) {
      cols = this.header.pins[0].length;
    }
    let hdr = [this.header.name];
    for (let i = 1; i < cols; i++) {
      hdr[i] = "";
    }
    data.setupTable(hdr);
    for (let y = 0; y < this.header.pins.length; y++) {
      let row = this.header.pins[y];
      let items = [];
      for (let x = 0; x < row.length; x++) {
        items[x] = document.createElement("gpio-view");
      }
      items = data.appendRow(items);
      for (let x = 0; x < items.length; x++) {
        items[x].setupPin(row[x]);
      }
    }
  }
});
</script>

<!-- A single I2C bus -->
<template id="template-i2c-elem">
  <data-table-elem></data-table-elem>
</template>
<script>
"use strict";
window.customElements.define("i2c-elem", class extends HTMLElementTemplate {
  constructor() {super("template-i2c-elem");}
  setupI2C(name, number, err, scl, sda) {
    let data = this.shadowRoot.querySelector("data-table-elem");
    data.setupTable([name, ""]);
    if (number != -1) {
      data.appendRow(["Number", number]);
    }
    if (err) {
      data.appendRow(["Error", err]);
    }
    if (scl) {
      data.appendRow(["SCL", scl]);
    }
    if (sda) {
      data.appendRow(["SDA", sda]);
    }
  }
});
</script>

<!-- A single SPI port -->
<template id="template-spi-elem">
  <data-table-elem></data-table-elem>
</template>
<script>
"use strict";
window.customElements.define("spi-elem", class extends HTMLElementTemplate {
  constructor() {super("template-spi-elem");}
  setupSPI(name, number, err, clk, mosi, miso, cs) {
    let data = this.shadowRoot.querySelector("data-table-elem");
    data.setupTable([name, ""]);
    if (number != -1) {
      data.appendRow(["Number", number]);
    }
    if (err) {
      data.appendRow(["Error", err]);
    }
    if (clk) {
      data.appendRow(["CLK", clk]);
    }
    if (mosi) {
      data.appendRow(["MOSI", mosi]);
    }
    if (mosi) {
      data.appendRow(["MISO", miso]);
    }
    if (cs) {
      data.appendRow(["CS", cs]);
    }
  }
});
</script>

<!-- *** Content *** -->

<div class="err" id="err"></div>
<h1>GPIO</h1>
<div id="section-gpio"></div>
<div id="section-state">
  <h1>periph's state</h1>
  <div>
    <drivers-elem id="section-drivers-loaded"></drivers-elem>
    <drivers-elem id="section-drivers-skipped"></drivers-elem>
    <drivers-elem id="section-drivers-failed"></drivers-elem>
  </div>
</div>
<h1>I²C</h1>
<div id="section-i2c"></div>
<h1>SPI</h1>
<div id="section-spi"></div>
